{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a8b892c8",
   "metadata": {},
   "source": [
    "Printed and electronic copies of *Modeling and Simulation in Python* are available from [No Starch Press](https://nostarch.com/modeling-and-simulation-python) and [Bookshop.org](https://bookshop.org/p/books/modeling-and-simulation-in-python-allen-b-downey/17836697?ean=9781718502161) and [Amazon](https://amzn.to/3y9UxNb)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "electric-netherlands",
   "metadata": {},
   "source": [
    "# Glucose and Insulin"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "imported-table",
   "metadata": {
    "tags": []
   },
   "source": [
    "*Modeling and Simulation in Python*\n",
    "\n",
    "Copyright 2021 Allen Downey\n",
    "\n",
    "License: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "formal-context",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# download modsim.py if necessary\n",
    "\n",
    "from os.path import basename, exists\n",
    "\n",
    "def download(url):\n",
    "    filename = basename(url)\n",
    "    if not exists(filename):\n",
    "        from urllib.request import urlretrieve\n",
    "        local, _ = urlretrieve(url, filename)\n",
    "        print('Downloaded ' + local)\n",
    "    \n",
    "download('https://github.com/AllenDowney/ModSimPy/raw/master/modsim.py')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "progressive-typing",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# import functions from modsim\n",
    "\n",
    "from modsim import *"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "preliminary-mexico",
   "metadata": {
    "tags": []
   },
   "source": [
    "This chapter is available as a Jupyter notebook where you can read the text, run the code, and work on the exercises. \n",
    "Click here to access the notebooks: <https://allendowney.github.io/ModSimPy/>."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "detected-welsh",
   "metadata": {},
   "source": [
    "The previous chapter presents the minimal model of the glucose-insulin system and introduces a tool we need to implement it: interpolation.\n",
    "\n",
    "In this chapter, we'll implement the model two ways:\n",
    "\n",
    "* We'll start by rewriting the differential equations as difference equations; then we'll solve the difference equations using a version of `run_simulation` similar to what we have used in previous chapters.\n",
    "\n",
    "* Then we'll use a new SciPy function, called `solve_ivp`, to solve the differential equation using a better algorithm.\n",
    "\n",
    "We'll see that `solve_ivp` is faster and more accurate than `run_simulation`.\n",
    "As a result, we will use it for the models in the rest of the book."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "original-photographer",
   "metadata": {
    "tags": []
   },
   "source": [
    "The following cell downloads the data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "fewer-weather",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "download('https://github.com/AllenDowney/ModSim/raw/main/data/' +\n",
    "         'glucose_insulin.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "postal-procedure",
   "metadata": {
    "tags": []
   },
   "source": [
    "We can use Pandas to read the data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "computational-border",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "from pandas import read_csv\n",
    "\n",
    "data = read_csv('glucose_insulin.csv', index_col='time');"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "collective-orleans",
   "metadata": {},
   "source": [
    "## Implementing the Model\n",
    "\n",
    "To get started, let's assume that the parameters of the model are known.\n",
    "We'll implement the model and use it to generate time series for `G` and `X`. \n",
    "Then we'll see how we can choose parameters that make the simulation fit the data.\n",
    "\n",
    "Here are the parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "delayed-trance",
   "metadata": {},
   "outputs": [],
   "source": [
    "G0 = 270\n",
    "k1 = 0.02\n",
    "k2 = 0.02\n",
    "k3 = 1.5e-05"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sapphire-examination",
   "metadata": {},
   "source": [
    "I'll put these values in a sequence which we'll pass to `make_system`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "following-alarm",
   "metadata": {},
   "outputs": [],
   "source": [
    "params = G0, k1, k2, k3"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "polished-burner",
   "metadata": {},
   "source": [
    "Here's a version of `make_system` that takes `params` and `data` as parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "substantial-literacy",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "def make_system(params, data):\n",
    "    G0, k1, k2, k3 = params\n",
    "    \n",
    "    t_0 = data.index[0]\n",
    "    t_end = data.index[-1]\n",
    "    \n",
    "    Gb = data.glucose[t_0]\n",
    "    Ib = data.insulin[t_0]\n",
    "    I = interpolate(data.insulin)\n",
    "    \n",
    "    init = State(G=G0, X=0)\n",
    "    \n",
    "    return System(init=init, params=params,\n",
    "                  Gb=Gb, Ib=Ib, I=I,\n",
    "                  t_0=t_0, t_end=t_end, dt=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afraid-friendly",
   "metadata": {},
   "source": [
    "`make_system` gets `t_0` and `t_end` from the data. \n",
    "It uses the measurements at `t=0` as the basal levels, `Gb` and `Ib`. \n",
    "And it uses the parameter `G0` as the initial value for `G`. Then it \n",
    "packs everything into a `System` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "legislative-richards",
   "metadata": {},
   "outputs": [],
   "source": [
    "system = make_system(params, data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stupid-retro",
   "metadata": {},
   "source": [
    "## The Update Function\n",
    "\n",
    "The minimal model is expressed in terms of differential equations:\n",
    "\n",
    "$$\\frac{dG}{dt} = -k_1 \\left[ G(t) - G_b \\right] - X(t) G(t)$$\n",
    "\n",
    "$$\\frac{dX}{dt} = k_3 \\left[I(t) - I_b \\right] - k_2 X(t)$$ \n",
    "\n",
    "To simulate this system, we will rewrite them as difference equations. \n",
    "If we multiply both sides by $dt$, we have:\n",
    "\n",
    "$$dG = \\left[ -k_1 \\left[ G(t) - G_b \\right] - X(t) G(t) \\right] dt$$\n",
    "\n",
    "$$dX = \\left[ k_3 \\left[I(t) - I_b \\right] - k_2 X(t) \\right] dt$$ \n",
    "\n",
    "If we think of $dt$ as a small step in time, these equations tell us how to compute the corresponding changes in $G$ and $X$.\n",
    "Here's an update function that computes these changes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "roman-archive",
   "metadata": {},
   "outputs": [],
   "source": [
    "def update_func(t, state, system):\n",
    "    G, X = state\n",
    "    G0, k1, k2, k3 = system.params \n",
    "    I, Ib, Gb = system.I, system.Ib, system.Gb\n",
    "    dt = system.dt\n",
    "        \n",
    "    dGdt = -k1 * (G - Gb) - X*G\n",
    "    dXdt = k3 * (I(t) - Ib) - k2 * X\n",
    "    \n",
    "    G += dGdt * dt\n",
    "    X += dXdt * dt\n",
    "\n",
    "    return State(G=G, X=X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "basic-subdivision",
   "metadata": {},
   "source": [
    "As usual, the update function takes a timestamp, a `State` object, and a `System` object as parameters. The first line uses multiple assignment to extract the current values of `G` and `X`.\n",
    "\n",
    "The following lines unpack the parameters we need from the `System`\n",
    "object.\n",
    "\n",
    "To compute the derivatives `dGdt` and `dXdt` we translate the equations from math notation to Python.\n",
    "Then, to perform the update, we multiply each derivative by the time step `dt`, which is 2 min in this example. \n",
    "\n",
    "The return value is a `State` object with the new values of `G` and `X`.\n",
    "\n",
    "Before running the simulation, it is a good idea to run the update\n",
    "function with the initial conditions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "sapphire-shannon",
   "metadata": {},
   "outputs": [],
   "source": [
    "update_func(system.t_0, system.init, system)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "growing-hormone",
   "metadata": {},
   "source": [
    "If it runs without errors and there is nothing obviously wrong with the results, we are ready to run the simulation. "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "strange-citation",
   "metadata": {},
   "source": [
    "## Running the Simulation\n",
    "\n",
    "We'll use the following version of `run_simulation`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "willing-masters",
   "metadata": {},
   "outputs": [],
   "source": [
    "def run_simulation(system, update_func):    \n",
    "    t_array = linrange(system.t_0, system.t_end, system.dt)\n",
    "    n = len(t_array)\n",
    "    \n",
    "    frame = TimeFrame(index=t_array, \n",
    "                      columns=system.init.index)\n",
    "    frame.iloc[0] = system.init\n",
    "    \n",
    "    for i in range(n-1):\n",
    "        t = t_array[i]\n",
    "        state = frame.iloc[i]\n",
    "        frame.iloc[i+1] = update_func(t, state, system)\n",
    "    \n",
    "    return frame"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "musical-loading",
   "metadata": {},
   "source": [
    "This version is similar to the one we used for the coffee cooling problem.\n",
    "The biggest difference is that it makes and returns a `TimeFrame`, which contains one column for each state variable, rather than a `TimeSeries`, which can only store one state variable.\n",
    "\n",
    "When we make the `TimeFrame`, we use `index` to indicate that the index is the array of time stamps, `t_array`, and `columns` to indicate that the column names are the state variables we get from `init`.\n",
    "\n",
    "We can run it like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "optional-burden",
   "metadata": {},
   "outputs": [],
   "source": [
    "results = run_simulation(system, update_func)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ambient-video",
   "metadata": {},
   "source": [
    "The result is a `TimeFrame` with a row for each time step and a column for each of the state variables, `G` and `X`.\n",
    "Here are the first few time steps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "international-germany",
   "metadata": {},
   "outputs": [],
   "source": [
    "results.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "least-steal",
   "metadata": {},
   "source": [
    "The following plot shows the simulated glucose levels from the model along with the measured data. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "liked-asset",
   "metadata": {},
   "outputs": [],
   "source": [
    "data.glucose.plot(style='o', alpha=0.5, label='glucose data')\n",
    "results.G.plot(style='-', color='C0', label='simulation')\n",
    "\n",
    "decorate(xlabel='Time (min)',\n",
    "         ylabel='Concentration (mg/dL)')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stopped-excuse",
   "metadata": {},
   "source": [
    "With the parameters I chose, the model fits the data well except during the first few minutes after the injection.\n",
    "But we don't expect the model to do well in this part of the time series.\n",
    "\n",
    "The problem is that the model is *non-spatial*; that is, it does not\n",
    "take into account different concentrations in different parts of the\n",
    "body. Instead, it assumes that the concentrations of glucose and insulin in blood, and insulin in tissue fluid, are the same throughout the body. This way of representing the body is known among experts as the \"bag of blood\" model.\n",
    "\n",
    "Immediately after injection, it takes time for the injected glucose to\n",
    "circulate. During that time, we don't expect a non-spatial model to be\n",
    "accurate. For this reason, we should not take the estimated value of `G0` too seriously; it is useful for fitting the model, but not meant to correspond to a physical, measurable quantity."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "internal-positive",
   "metadata": {},
   "source": [
    "The following plot shows simulated insulin levels in the hypothetical \"remote compartment\", which is in unspecified units."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "damaged-protection",
   "metadata": {},
   "outputs": [],
   "source": [
    "results.X.plot(color='C1', label='remote insulin')\n",
    "\n",
    "decorate(xlabel='Time (min)', \n",
    "         ylabel='Concentration (arbitrary units)')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "economic-spare",
   "metadata": {},
   "source": [
    "Remember that `X` represents the concentration of insulin in the \"remote compartment\", which is believed to be tissue fluid, so we can't compare it to the measured concentration of insulin in the blood.\n",
    "\n",
    "`X` rises quickly after the initial injection and then declines as the concentration of glucose declines.  Qualitatively, this behavior is as expected, but because `X` is not an observable quantity, we can't validate this part of the model quantitatively."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spatial-scholar",
   "metadata": {},
   "source": [
    "## Solving Differential Equations\n",
    "\n",
    "To implement the minimal model, we rewrote the differential equations as difference equations with a finite time step, `dt`.\n",
    "When $dt$ is very small, or more precisely *infinitesimal*, the difference equations are the same as the differential equations.\n",
    "But in our simulations, $dt$ is 2 min, which is not very small, and definitely not infinitesimal. \n",
    "\n",
    "In effect, the simulations assume that the derivatives $dG/dt$ and $dX/dt$ are constant during each 2 min time step.\n",
    "This method, evaluating derivatives at discrete time steps and assuming that they are constant in between, is called *Euler's method* (see <http://modsimpy.com/euler>).\n",
    "\n",
    "Euler's method is good enough for many problems, but sometimes it is not very accurate.\n",
    "In that case, we can usually make it more accurate by decreasing the size of `dt`.\n",
    "But then it is not very efficient.\n",
    "\n",
    "There are other methods that are more accurate and more efficient than Euler's method.\n",
    "SciPy provides several of them wrapped in a function called `solve_ivp`.\n",
    "The `ivp` stands for *initial value problem*, which is the term for problems like the ones we've been solving, where we are given the initial conditions and try to predict what will happen.\n",
    "\n",
    "The ModSim library provides a function called `run_solve_ivp` that makes `solve_ivp` a little easier to use.\n",
    "\n",
    "To use it, we have to provide a *slope function*, which is similar to an update function; in fact, it takes the same parameters: a time stamp, a `State` object, and a `System` object.\n",
    "\n",
    "Here's a slope function that evaluates the differential equations of the minimal model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "expected-collapse",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "def slope_func(t, state, system):\n",
    "    G, X = state\n",
    "    G0, k1, k2, k3 = system.params \n",
    "    I, Ib, Gb = system.I, system.Ib, system.Gb\n",
    "        \n",
    "    dGdt = -k1 * (G - Gb) - X*G\n",
    "    dXdt = k3 * (I(t) - Ib) - k2 * X\n",
    "    \n",
    "    return dGdt, dXdt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "modified-surname",
   "metadata": {},
   "source": [
    "`slope_func` is a little simpler than `update_func` because it computes only the derivatives, that is, the slopes. It doesn't do the updates; the solver does them for us.\n",
    "\n",
    "Now we can call `run_solve_ivp` like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "intelligent-visitor",
   "metadata": {},
   "outputs": [],
   "source": [
    "results2, details = run_solve_ivp(system, slope_func,\n",
    "                                  t_eval=results.index)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "promotional-result",
   "metadata": {},
   "source": [
    "`run_solve_ivp` is similar to `run_simulation`: it takes a `System`\n",
    "object and a slope function as parameters.\n",
    "\n",
    "The third argument, `t_eval`, is optional; it specifies where the solution should be evaluated.\n",
    "\n",
    "It returns two values: a `TimeFrame`, which we assign to `results2`, and an `OdeResult` object, which we assign to `details`.\n",
    "\n",
    "The `OdeResult` object contains information about how the solver ran, including a success code and a diagnostic message."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "industrial-focus",
   "metadata": {},
   "outputs": [],
   "source": [
    "details.success"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "frequent-exhibit",
   "metadata": {},
   "outputs": [],
   "source": [
    "details.message"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fatal-parliament",
   "metadata": {},
   "source": [
    "It's important to check these messages after running the solver, in case anything went wrong.\n",
    "\n",
    "The `TimeFrame` has one row for each time step and one column for each state variable. In this example, the rows are time from 0 to 182 minutes; the columns are the state variables, `G` and `X`.\n",
    "Here are the first few time steps:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "immediate-legislature",
   "metadata": {},
   "outputs": [],
   "source": [
    "results2.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "noticed-material",
   "metadata": {},
   "source": [
    "Because we used `t_eval=results.index`, the time stamps in `results2` are the same as in `results`, which makes them easier to compare.\n",
    "\n",
    "The following figure shows the results from `run_solve_ivp` along with the results from `run_simulation`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "peripheral-commission",
   "metadata": {},
   "outputs": [],
   "source": [
    "results.G.plot(style='--', label='simulation')\n",
    "results2.G.plot(style='-', label='solve ivp')\n",
    "\n",
    "decorate(xlabel='Time (min)',\n",
    "         ylabel='Concentration (mg/dL)')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "advanced-provider",
   "metadata": {},
   "source": [
    "The differences are barely visible.\n",
    "We can compute the relative differences like this:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "diagnostic-lawsuit",
   "metadata": {},
   "outputs": [],
   "source": [
    "diff = results.G - results2.G\n",
    "percent_diff = diff / results2.G * 100"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "comparative-boulder",
   "metadata": {},
   "source": [
    "And we can use `describe` to compute summary statistics:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "clear-neighbor",
   "metadata": {},
   "outputs": [],
   "source": [
    "percent_diff.abs().describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "temporal-threat",
   "metadata": {},
   "source": [
    "The mean relative difference is about 0.65% and the maximum is a little more than 1%.\n",
    "Here are the results for `X`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "happy-guess",
   "metadata": {},
   "outputs": [],
   "source": [
    "results.X.plot(style='--', label='simulation')\n",
    "results2.X.plot(style='-', label='solve ivp')\n",
    "\n",
    "decorate(xlabel='Time (min)', \n",
    "         ylabel='Concentration (arbitrary units)')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "electronic-navigation",
   "metadata": {},
   "source": [
    "These differences are a little bigger, especially at the beginning.\n",
    "\n",
    "If we use `run_simulation` with smaller time steps, the results are more accurate, but they take longer to compute.\n",
    "For some problems, we can find a value of `dt` that produces accurate results in a reasonable time. However, if `dt` is *too* small, the results can be inaccurate again. So it can be tricky to get it right.\n",
    "\n",
    "The advantage of `run_solve_ivp` is that it chooses the step size automatically in order to balance accuracy and efficiency.\n",
    "You can use keyword arguments to adjust this balance, but most of the time the results are accurate enough, and the computation is fast enough, without any intervention."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "julian-dublin",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "diff = results.G - results2.X\n",
    "percent_diff = diff / results2.X * 100\n",
    "percent_diff.abs().describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "prostate-psychology",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this chapter, we implemented the glucose minimal model two ways, using `run_simulation` and `run_solve_ivp`, and compared the results.\n",
    "We found that in this example, `run_simulation`, which uses Euler's method, is probably good enough.\n",
    "But soon we will see examples where it is not.\n",
    "\n",
    "So far, we have assumed that the parameters of the system are known, but in practice that's not true.\n",
    "As one of the case studies in the next chapter, you'll have a chance to see where those parameters came from."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "unusual-springer",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "This chapter is available as a Jupyter notebook where you can read the text, run the code, and work on the exercises. \n",
    "You can access the notebooks at <https://allendowney.github.io/ModSimPy/>."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "substantial-grain",
   "metadata": {},
   "source": [
    "### Exercise 1\n",
    "\n",
    "Our solution to the differential equations is only approximate because we used a finite step size, `dt=2` minutes.\n",
    "If we make the step size smaller, we expect the solution to be more accurate.  Run the simulation with `dt=1` and compare the results.  What is the largest relative error between the two solutions?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "thermal-trance",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "prompt-activity",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "developed-collaboration",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "russian-qualification",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
